import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlKeysetPagination } from '@gitlab/ui';
import VueRouter from 'vue-router';
import { shallowMount } from '@vue/test-utils';
import VulnerabilityListGraphql, {
  PAGE_SIZE_STORAGE_KEY,
} from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue';
import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue';
import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants';
import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { FIELDS } from 'ee/security_dashboard/components/shared/vulnerability_report/constants';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { vulnerabilities } from '../../mock_data';

jest.mock('~/alert');

Vue.use(VueApollo);
Vue.use(VueRouter);
const router = new VueRouter();

const fullPath = 'path';
const portalName = 'portal-name';
// Sort object used by tests that don't need to care about what the values are.
const SORT_OBJECT = { sortBy: 'detected', sortDesc: true };
const DEFAULT_SORT = `${FIELDS.SEVERITY.key}_desc`;

const createVulnerabilitiesRequestHandler = (options) =>
  jest.fn().mockResolvedValue({
    data: {
      group: {
        id: 'group-1',
        __typename: 'Group',
        vulnerabilities: {
          nodes: vulnerabilities,
          pageInfo: {
            __typename: 'PageInfo',
            startCursor: 'abc',
            endCursor: 'def',
            hasNextPage: true,
            hasPreviousPage: false,
            ...options,
          },
        },
      },
    },
  });

const vulnerabilitiesRequestHandler = createVulnerabilitiesRequestHandler();

describe('Vulnerability list GraphQL component', () => {
  let wrapper;

  const createWrapper = ({
    vulnerabilitiesHandler = vulnerabilitiesRequestHandler,
    canViewFalsePositive = false,
    showProjectNamespace = false,
    hasJiraVulnerabilitiesIntegrationEnabled = false,
    filters = {},
    fields = [],
    stubs = {},
  } = {}) => {
    wrapper = shallowMount(VulnerabilityListGraphql, {
      router,
      apolloProvider: createMockApollo([[vulnerabilitiesQuery, vulnerabilitiesHandler]]),
      provide: {
        fullPath,
        dashboardType: DASHBOARD_TYPES.GROUP,
        vulnerabilitiesQuery,
        canViewFalsePositive,
        hasJiraVulnerabilitiesIntegrationEnabled,
      },
      propsData: {
        portalName,
        filters,
        fields,
        showProjectNamespace,
      },
      stubs,
    });
  };

  const findVulnerabilityList = () => wrapper.findComponent(VulnerabilityList);
  const findPagination = () => wrapper.findComponent(GlKeysetPagination);
  const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector);
  const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);

  afterEach(() => {
    vulnerabilitiesRequestHandler.mockClear();
    // Reset querystring.
    if (Object.keys(router.currentRoute.query).length) {
      router.push({ query: undefined });
    }
    localStorage.clear();
  });

  describe('vulnerabilities query', () => {
    it('calls the query once with the expected fullPath variable', () => {
      createWrapper();

      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledTimes(1);
      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({ fullPath }),
      );
    });

    it.each([true, false])(
      'calls the query with the expected vetEnabled property when canViewFalsePositive is %s',
      (canViewFalsePositive) => {
        createWrapper({ canViewFalsePositive });

        expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
          expect.objectContaining({ vetEnabled: canViewFalsePositive }),
        );
      },
    );

    it.each([true, false])(
      'calls the query with the expected includeExternalIssueLinks property when hasJiraVulnerabilitiesIntegrationEnabled is %s',
      (hasJiraVulnerabilitiesIntegrationEnabled) => {
        createWrapper({ hasJiraVulnerabilitiesIntegrationEnabled });

        expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
          expect.objectContaining({
            includeExternalIssueLinks: hasJiraVulnerabilitiesIntegrationEnabled,
          }),
        );
      },
    );

    it('does not call the query if filters are not ready', () => {
      createWrapper({ filters: null });

      expect(vulnerabilitiesRequestHandler).not.toHaveBeenCalled();
    });

    it('shows an error message if the query fails', async () => {
      const vulnerabilitiesHandler = jest.fn().mockRejectedValue(new Error());
      createWrapper({ vulnerabilitiesHandler });
      await waitForPromises();

      expect(createAlert).toHaveBeenCalled();
    });

    it('emits the query-variables-changed event when the query variables changes', async () => {
      createWrapper({ filters: { abc: 123 } });
      await wrapper.setProps({ filters: { abc: 456 } });

      expect(wrapper.emitted('query-variables-changed')).toHaveLength(1);
      expect(wrapper.emitted('query-variables-changed')[0][0]).toMatchObject({ abc: 456 });
    });

    it('does not emit the query-variables-changed event when the query variables changes, but no values changed', async () => {
      createWrapper({ filters: { abc: 123 } });
      await wrapper.setProps({ filters: { abc: 123 } });

      expect(wrapper.emitted('query-variables-changed')).toBeUndefined();
    });

    it('triggers the query only once when retrieving the page size from the local storage', async () => {
      const pageSize = 50;
      localStorage.setItem(PAGE_SIZE_STORAGE_KEY, pageSize);
      createWrapper({ stubs: { LocalStorageSync } });
      await nextTick();

      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledTimes(1);
      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({ first: pageSize }),
      );
    });

    it("marks the query to be a single request to reduce the query's complexity score", () => {
      expect(wrapper.vm.$apollo.queries.vulnerabilities.options.context.isSingleRequest).toBe(true);
    });
  });

  describe('vulnerability list component', () => {
    it('gets the expected props', () => {
      const fields = ['abc'];
      const showProjectNamespace = true;
      createWrapper({ fields, showProjectNamespace });

      expect(findVulnerabilityList().props()).toMatchObject({
        shouldShowProjectNamespace: showProjectNamespace,
        fields,
        portalName,
      });
    });

    it('calls the GraphQL query with the expected sort data when the vulnerability list changes the sort', async () => {
      createWrapper();
      vulnerabilitiesRequestHandler.mockClear();
      findVulnerabilityList().vm.$emit('update:sort', SORT_OBJECT);
      await nextTick();

      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({ sort: 'detected_desc' }),
      );
    });
  });

  describe('sorting', () => {
    it.each`
      sortBy           | sortDesc | expected
      ${'state'}       | ${true}  | ${'state_desc'}
      ${'detected'}    | ${false} | ${'detected_asc'}
      ${'description'} | ${true}  | ${'description_desc'}
    `(
      'reads the querystring sort info and calls the GraphQL query with "$expected" for sort',
      ({ sortBy, sortDesc, expected }) => {
        router.push({ query: { sortBy, sortDesc } });
        createWrapper();

        // This is important; we want the sort info to be read from the querystring from the
        // beginning so that the GraphQL request is only done once, instead of starting with the
        // default sort, then immediately reading the querystring value, which will trigger the
        // GraphQL request twice.
        expect(vulnerabilitiesRequestHandler).toHaveBeenCalledTimes(1);
        expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
          expect.objectContaining({ sort: expected }),
        );
      },
    );

    it(`uses the default sort if there's no querystring data`, () => {
      createWrapper();

      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({ sort: DEFAULT_SORT }),
      );
    });

    it('passes the sort data to the vulnerability list', () => {
      router.push({ query: SORT_OBJECT });
      createWrapper();

      expect(findVulnerabilityList().props('sort')).toEqual(SORT_OBJECT);
    });

    it('calls the GraphQL query with the expected sort data when the vulnerability list changes the sort', async () => {
      createWrapper();
      findVulnerabilityList().vm.$emit('update:sort', SORT_OBJECT);
      await nextTick();

      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({ sort: 'detected_desc' }),
      );
    });

    it.each`
      sortBy           | sortDesc
      ${'state'}       | ${true}
      ${'detected'}    | ${false}
      ${'description'} | ${true}
    `(
      'updates the querystring to sortBy = "$sortBy", sortDesc = "$sortDesc" when the vulnerability list changes the sort',
      ({ sortBy, sortDesc }) => {
        createWrapper();
        findVulnerabilityList().vm.$emit('update:sort', { sortBy, sortDesc });

        expect(router.currentRoute.query).toMatchObject({ sortBy, sortDesc: sortDesc.toString() });
      },
    );

    // This test is for when the querystring is changed by the user clicking forward/back in the
    // browser. When a history state is pushed that only changes the querystring, the page does not
    // refresh, so we need to handle the case where the user is stepping through the browser history
    // but no page refresh is done.
    it('calls the GraphQL query with the expected sort data when the querystring is changed', async () => {
      createWrapper();
      router.push({ query: { sortBy: 'detected', sortDesc: true } });
      await nextTick();

      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({ sort: 'detected_desc' }),
      );
    });

    it('will reset paging if the sort has changed', async () => {
      createWrapper();
      await waitForPromises();
      router.push({ query: { after: 'abc' } });
      findVulnerabilityList().vm.$emit('update:sort', SORT_OBJECT);
      await nextTick();

      expect(router.currentRoute.query.after).toBeUndefined();
    });

    it('will not reset paging when the page changes but sorting has not', async () => {
      router.push({ query: SORT_OBJECT });
      createWrapper();
      await waitForPromises();
      router.push({ query: { ...router.currentRoute.query, after: 'abc' } });
      await nextTick();

      expect(router.currentRoute.query.after).toBe('abc');
    });
  });

  describe('pagination', () => {
    it.each([
      { startCursor: 'abc', endCursor: 'def', hasPreviousPage: true, hasNextPage: false },
      { startCursor: 'ghi', endCursor: 'jkl', hasPreviousPage: false, hasNextPage: true },
    ])('has the expected props for pageInfo %s', async (pageInfo) => {
      const vulnerabilitiesHandler = createVulnerabilitiesRequestHandler(pageInfo);
      createWrapper({ vulnerabilitiesHandler });
      await waitForPromises();

      expect(findPagination().props()).toMatchObject(pageInfo);
    });

    it('syncs the disabled prop with the query loading state', async () => {
      createWrapper();
      // When the component is mounted, the Apollo query will immediately start to load.
      expect(findPagination().props('disabled')).toBe(true);
      await waitForPromises();
      // After the query handler is resolved, the Apollo query will no longer be loading.
      expect(findPagination().props('disabled')).toBe(false);
    });

    it.each`
      navigationDirection | expectedQueryVariables
      ${'next'}           | ${{ after: 'endCursor' }}
      ${'prev'}           | ${{ before: 'startCursor' }}
    `(
      'navigates to the $navigationDirection page',
      async ({ navigationDirection, expectedQueryVariables }) => {
        const pageInfo = { startCursor: 'startCursor', endCursor: 'endCursor' };
        const vulnerabilitiesHandler = createVulnerabilitiesRequestHandler(pageInfo);
        createWrapper({ vulnerabilitiesHandler });
        await waitForPromises();

        findPagination().vm.$emit(navigationDirection);
        await nextTick();

        expect(vulnerabilitiesHandler).toHaveBeenLastCalledWith(
          expect.objectContaining(expectedQueryVariables),
        );
      },
    );
  });

  describe('page size selector', () => {
    const expectPageSizeUsed = (pageSize) => {
      expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({ first: pageSize }),
      );
      // Vulnerability list needs the page size to show the correct number of skeleton loaders.
      expect(findVulnerabilityList().props('pageSize')).toBe(pageSize);
    };

    beforeEach(() => {
      createWrapper();
    });

    it('uses the default page size if page size selector was not changed', () => {
      expectPageSizeUsed(DEFAULT_PAGE_SIZE);
    });

    it('uses the page size selected by the page size selector', async () => {
      const pageSize = 50;
      findPageSizeSelector().vm.$emit('input', pageSize);
      await nextTick();

      expectPageSizeUsed(pageSize);
    });

    it('sets up the local storage sync correctly', async () => {
      const pageSize = 123;
      findPageSizeSelector().vm.$emit('input', pageSize);
      await nextTick();

      expect(findLocalStorageSync().props('value')).toBe(pageSize);
    });
  });
});
